HTTP : Cache-Control은 어떻게 사용될까?
캐싱은 주어진 리소스의 복사본을 저장하고 있다가 요청 시에 그것을 제공하는 기술로, 웹 캐시가 자신의 저장소 내에 요청된 리소스를 가지고 있다면, 요청을 가로채 원래의 서버로부터 리소스를 다시 다운로드하는 대신 리소스의 복사본을 반환한다.
웹 캐시는 레이턴시와 네트워크 트래픽을 줄여줌으로써 리소스를 보여주는 데에 필요한 시간을 줄여준다(성능 향상 및 네트워크 비용 절감). 다만 모든 리소스가 영원히 변하지 않는 것은 아니므로, 리소스가 변하기 전까지만 캐싱하고 변한 이후에는 더 이상 캐싱하지 않는 것이 중요하다(무효화 정책의 중요성).
캐시되는 리소스들
HTTP는 일반적으로 GET 요청에 대한 응답만 캐시되며
캐시키는 요청 메서드 + 대상 URI로 구성되거나, GET만 사용한다면 URI로만 구성된다.
일반적인 캐싱 엔트리는 다음과 같다.
- 검색 요청의 성공적인 결과 : HTML, 이미지 혹은 파일 Get 요청에 따른 200 OK의 응답
- 영구적 Redirect인 301에 대한 응답
- 오류 응답 404 결과 페이지 등
브라우저에서 어떤 URL을 입력했을 때, 웹서버가 응답해주는 리소스들을 브라우저 캐시 저장소에 저장되게 된다. 물론 아래에 나오는 캐시제어에 따라 저장되거나 안될 수도 있으며 캐시가 삭제될 수도 있다.
GraphQL은 POST만 쓰는데 캐시는 어떻게 동작하는 걸까
우선 GraphQL은 Post로 요청하고 단일 URI에 요청을 보낸다. 그래서 브라우저 캐시가 동작할 수 없다. 그렇기 때문에, GraphQL 클라이언트들(ex. Apollo-Client)은 자체 캐시 레이어를 구축해서 HTTP 캐시 대신 어플리케이션 캐시를 사용한다.
하지만 GET요청도 Body를 담을 수 있기 때문에, Query 요청을 Get 메소드로 수정할 수는 있다. 그러면 HTTP 캐시나 CDN도 정상적으로 동작할 것이다. 그러나 URI 길이 제한이 있으므로 안전하지는 않다.
캐시의 종류 나누기 : Private or Public
Cache-Control : public
한 명 이상의 유저에게 재사용되는 응답들을 저장한 캐시다.
예를 들어 Web Proxy 같은 것을 예로 들 수 있는데, 트래픽 비용을 아끼기 위해서 대학교에는 API Gateway(외부와 단일 출구)에 Proxy를 두어
요청에 대한 응답값들을 캐시해두고 다른 유저가 똑같은 사이트를 방문했을때 외부 트래픽을 타지 않고 Proxy에서 바로 응답을 해주도록 설정을 해뒀을 가능성이 있다.
Cache-Control : private
단일 사용자가 전용으로, 브라우저마다 설정된 Cache 같은 것을 의미합니다. 대신 브라우저는 HTTP를 통해 다운로드된 모든 문서들을 브라우저 저장소에 캐시해두고 똑같은 요청이 발생하면 네트워크 요청 대신 저장소에서 꺼내쓰게 된다.
Cloudfront같은 CDN을 사용중이더라도, private 디렉티브가 있으면, CDN은 캐싱하지 않는다.
만약 캐시가 무효화되는 시점이면, 브라우저는 CDN을 거치면서 유효성 검증을 진행하는데, 아직 캐시가 유효하다면 Origin 까지 넘어가지 않고 CDN 단에서 캐시가 유효한지 여부를 판단해준다.
캐시 유효성 검증은 아래에 자세히 있다.
캐시 수명 제어
캐시를 수명을 제어하기 위해, HTTP 요청과 응답에 각각 Cache-Control 헤더에 디렉티브를 지정하거나 Expires헤더를 쓴다.
웹서버나 CDN에서 응답에 아래와 같은 헤더가 있다면 지정된 일자나, 시간만큼의 캐시 수명을 갖게된다.
Expires : <HTTP-date>- 명시된 날짜까지만 유효하며, 만료되기 전까지 캐시가 유효하다.
Cache-Control : max-age=<seconds>:- 응답을 받은 이후 seconds 만큼 캐시가 유효하다.
Expires디렉티브보다 우선순위 높다.
Cache-Control : no-store- 클라이언트에선 절대 캐싱하지 않는다. 항상 원서버로 접근해서 응답을 받아온다.
Cache-Control : no-cache- 클라이언트에서 캐싱을 하지만, 항상 서버로부터 유효성 검증을 받아야한다.
Stale-While-Revalidate
SWR(Stale-While-Revalidate) 는 즉시성과 최신성의 균형을 맞추는 전략으로,
사용자는 캐시된 데이터를 즉시 받아 페이지 로딩이 빠르게 느껴지게 만들고
동시에, 백그라운드에서 최신 데이터를 가져와 캐시를 갱신함으로써 다음 요청에는 최신 데이터를 보여준다.
Cache-Control : stale-while-revalidate=30 처럼 사용할 수 있다.
이 수명 제어 디렉티브들은 쉼표로 연결되며 여러 제어를 담을 수 있다.
- 예를 들어,
Cache-Control : max-age=10, stale-while-revalidate=50의 경우- 처음 10초 동안: 캐시가 유효한(fresh) 상태이므로, 캐시된 데이터를 사용합니다.
- 10초~60초(10+50) 동안: 캐시가 부실한(stale) 상태가 되지만, SWR이 활성화됩니다.
- 클라이언트에게는 부실한 캐시 데이터를 즉시 제공합니다.
- 동시에, 백그라운드에서 서버에 새 데이터를 요청(재검증)합니다.
- 60초 이후: 캐시가 완전히 만료됩니다. 다음 요청 시에는 캐시 데이터를 즉시 제공하지 않고, 서버에서 새 데이터를 받아온 후 클라이언
React-Query의 SWR과 HTTP Cache-Control 헤더의 SWR 전략의 차이점
HTTP 헤더에서 SWR은 브라우저나 CDN 레벨에서 동작한다. 모든 HTTP 요청에 적용되므로 Public Cache 용도다. 그래서 현재 유저한테는 stale한 데이터를 보여주면서, 백그라운드에서 갱신된 캐시는 그 다음 사용자한테 보이게 된다.
반면, React-Query는 SWR 전략을 차용한 애플리케이션 레벨의 캐시로 Private Cache 용도라고 생각할 수 있다. 내부에서 데이터 캐싱과 재검증 로직을 관리하는데, 보통 현재 유저한테 stale한 데이터를 먼저 보여주고, 백그라운드에서 갱신된 데이터를 업데이트해서 현재 유저에게 다시 보여주는데 쓰인다.
캐시 유효성 검증
유효 수명이 있다면 브라우저는 캐시를 바로 사용하며, 캐시가 유효한 시간이 지나거나, no-cache인 경우에 유효성 검증을 진행하게 된다.
수명에 따른 판단
위 섹션에서 나오듯 유효성은 Cache-Control의 max-age, Expires 디렉티브에 따른 수명을 우선적으로 따르게 된다.
만약 응답헤더에 저런 디렉티브가 없다면, 브라우저는 응답 헤더의 Last-Modified를 살펴서 있다면, 만료시간을 계산해서 캐시에 저장한다.
이렇게 수명을 우선적으로 따라서 브라우저는 캐시를 사용할지 말지를 결정한다.
유효성 검증 수단 ETag, Last-Modified
-
ETag헤더- ETag는 리소스의 고유 식별자(버전 태그)
- 응답 헤더에
ETag헤더가 있다면, 브라우저는 이것과 함께 리소스를 캐시에 저장 - 리소스가 stale 해졌다면,
If-None-Match : <ETag 값>헤더를 담아 유효성 검증 요청
-
Last-Modified헤더- 리소스의 마지막 변경 날짜를 담은 헤더
- 응답 헤더에
Last-Modified헤다가 있다면, 브라우저는 이것과 함께 리소스를 캐시에 저장 - 리소스가 stale 해졌다면,
If-Modified-Since : <Last-Modified 값>헤더를 담아 유효성 검증 요청
유효성 검증 요청(조건부 요청)
이 헤더들은 모두 검증을 위한 도구이며 응답헤더에 ETag, Last-Modified 가 있어야만 조건부 요청이 동작한다.
또, Cache-Control 디렉티브에 따라 검증을 하기도, 안하기도 합니다. 브라우저는 Cache-Control의 정책(max-age, no-cache 등)에 따라
리소스가 stale 상태가 되었을 때만 If-None-Matched, If-Modified-Since 헤더를 붙여 검증 요청을 보냅니다.
유효성 검사 요청을 보내고 서버는 아직 캐시가 유효하다면, 본문없는 빠른 응답인 304 응답을 내려주며, 만약 ETag나 Last-Modified 값이 맞지 않다면 200OK + 본문을 내려주게 됩니다.
만약 no-cache를 썼는데, expires나 max-age, ETag, Last-Modified가 없으면 no-store와 같아진다.
참고
ETag는 강한 검증(Strong Validation), Last-Modified는 약한 검증(Weak Validation) 입니다. ETag는 1바이트라도 변경되면 다르게 판단하지만, Last-Modified는 초 단위까지만 비교하기 때문에 미세 변경은 감지하지 못할 수 있음. 두 헤더가 함께 존재하면, 브라우저는 ETag를 우선적으로 사용합니다.
References
https://web.dev/articles/content-delivery-networks?hl=ko#improving_cache_hit_ratio https://developer.mozilla.org/ko/docs/Web/HTTP/Guides/Caching https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Headers/ETag